17  비모수 검정

Keywords

python, 전처리, 통계, 가설검정, 기계학습, 회귀, 분류, 군집, 모델 학습, 모델 평가

비모수 검정(Non-parametric Tests)은 모집단의 분포에 대한 가정 없이 데이터를 분석하는 통계적 방법이다. 정규성이나 등분산성 같은 모수 검정의 엄격한 가정을 만족하지 못할 때, 또는 데이터가 순서형이거나 이상치가 많을 때 사용한다. 비모수 검정은 원 데이터 값 대신 순위(rank)를 사용하여 집단 간 차이를 검정하므로 분포에 덜 민감하고 강건하다. 이 장에서는 주요 비모수 검정 방법과 그 적용 상황을 학습한다.

예제: 데이터 로드

import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# 데이터 로드
df = sns.load_dataset("penguins")

print("데이터 크기:", df.shape)
print("\n범주형 변수:", df.select_dtypes(include=['object', 'category']).columns.tolist())
print("연속형 변수:", df.select_dtypes(include=[np.number]).columns.tolist())
데이터 크기: (344, 7)

범주형 변수: ['species', 'island', 'sex']
연속형 변수: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']

17.1 비모수 검정의 개념과 필요성

비모수 검정은 모집단의 분포 형태를 가정하지 않고, 데이터의 순위 정보만을 사용하여 검정한다.

모수 검정 vs 비모수 검정

구분 모수 검정 비모수 검정
가정 정규분포, 등분산성 가정 최소 (분포 자유)
사용 정보 원 데이터 값 순위(rank)
검정 대상 평균 중앙값 또는 분포 위치
검정력 가정 만족 시 높음 가정 미만족 시 높음
이상치 민감도 높음 낮음 (강건함)
해석 평균 차이 분포 위치 차이

17.1.1 비모수 검정이 필요한 상황

비모수 검정 적용 시점

상황 설명 확인 방법
정규성 위배 데이터가 정규분포를 따르지 않음 Shapiro-Wilk 검정, Q-Q plot
소표본 표본 크기가 작아 중심극한정리 적용 불가 n < 30 (특히 n < 15)
이상치 다수 극단값이 많아 평균이 왜곡됨 박스플롯, IQR 분석
순서형 데이터 측정 단위가 순서 척도 설문 리커트 척도 등
불균형 표본 집단 간 표본 크기 차이가 큼 표본 크기 비교

예제: 정규성 확인

# 정규성 검정
adelie = df[df["species"] == "Adelie"]["bill_length_mm"].dropna()
gentoo = df[df["species"] == "Gentoo"]["bill_length_mm"].dropna()

print("=== 정규성 검정 (Shapiro-Wilk) ===")
_, p_adelie = stats.shapiro(adelie)
_, p_gentoo = stats.shapiro(gentoo)

print(f"Adelie: p = {p_adelie:.4f} {'(정규)' if p_adelie > 0.05 else '(비정규)'}")
print(f"Gentoo: p = {p_gentoo:.4f} {'(정규)' if p_gentoo > 0.05 else '(비정규)'}")

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

stats.probplot(adelie, dist="norm", plot=axes[0])
axes[0].set_title("Q-Q Plot: Adelie")

stats.probplot(gentoo, dist="norm", plot=axes[1])
axes[1].set_title("Q-Q Plot: Gentoo")

plt.tight_layout()
plt.show()
=== 정규성 검정 (Shapiro-Wilk) ===
Adelie: p = 0.7166 (정규)
Gentoo: p = 0.0135 (비정규)

17.2 Mann-Whitney U 검정 (Wilcoxon Rank-Sum Test)

Mann-Whitney U 검정은 두 독립 집단의 분포 위치를 비교하는 비모수 검정으로, 독립표본 t-검정의 대안이다.

특징

  • 대상: 두 독립 집단
  • 대안 검정: 독립표본 t-검정
  • 검정 대상: 두 집단의 분포가 같은지 (중앙값 또는 순위 합)
  • 가정: 두 집단의 분포 형태가 유사 (위치만 다를 수 있음)

가설 설정

  • H₀ (귀무가설): 두 집단의 분포가 동일하다 (중앙값이 같다)
  • H₁ (대립가설): 두 집단의 분포가 다르다 (중앙값이 다르다)

검정 원리

  1. 두 집단의 데이터를 합쳐서 순위 매김
  2. 각 집단의 순위 합 계산
  3. U 통계량 계산 (작은 집단의 순위 합이 작을수록 차이가 큼)

17.2.1 예제: Adelie vs Gentoo 부리 길이 비교

예제: 데이터 준비

from scipy.stats import mannwhitneyu

# 두 종 선택
g1 = df[df["species"] == "Adelie"]["bill_length_mm"].dropna()
g2 = df[df["species"] == "Gentoo"]["bill_length_mm"].dropna()

print("=== 집단 정보 ===")
print(f"Adelie: n = {len(g1)}, 중앙값 = {g1.median():.2f}mm")
print(f"Gentoo: n = {len(g2)}, 중앙값 = {g2.median():.2f}mm")

# 기술 통계량
print("\n=== 기술 통계량 ===")
print("Adelie:")
print(g1.describe())
print("\nGentoo:")
print(g2.describe())
=== 집단 정보 ===
Adelie: n = 151, 중앙값 = 38.80mm
Gentoo: n = 123, 중앙값 = 47.30mm

=== 기술 통계량 ===
Adelie:
count    151.000000
mean      38.791391
std        2.663405
min       32.100000
25%       36.750000
50%       38.800000
75%       40.750000
max       46.000000
Name: bill_length_mm, dtype: float64

Gentoo:
count    123.000000
mean      47.504878
std        3.081857
min       40.900000
25%       45.300000
50%       47.300000
75%       49.550000
max       59.600000
Name: bill_length_mm, dtype: float64

예제: Mann-Whitney U 검정

# Mann-Whitney U 검정
u_stat, p_value = mannwhitneyu(g1, g2, alternative="two-sided")

print("\n=== Mann-Whitney U 검정 ===")
print(f"U-통계량: {u_stat:.4f}")
print(f"p-value: {p_value:.4f}")

alpha = 0.05
print(f"\n유의수준 {alpha} 기준:")
if p_value < alpha:
    print("✓ 귀무가설 기각: 두 종의 부리 길이 분포가 유의하게 다름")
    print(f"  중앙값 차이: {g2.median() - g1.median():.2f}mm")
else:
    print("✗ 귀무가설 채택: 두 종의 부리 길이 분포가 유의하게 다르지 않음")

=== Mann-Whitney U 검정 ===
U-통계량: 224.5000
p-value: 0.0000

유의수준 0.05 기준:
✓ 귀무가설 기각: 두 종의 부리 길이 분포가 유의하게 다름
  중앙값 차이: 8.50mm

예제: 시각화

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 박스플롯
data_combined = pd.DataFrame({
    'Bill Length': pd.concat([g1, g2]),
    'Species': ['Adelie']*len(g1) + ['Gentoo']*len(g2)
})
sns.boxplot(x='Species', y='Bill Length', data=data_combined, ax=axes[0])
axes[0].set_title("Bill Length: Adelie vs Gentoo")
axes[0].set_ylabel("Bill Length (mm)")

# 히스토그램
axes[1].hist(g1, alpha=0.5, label='Adelie', bins=15, edgecolor='black')
axes[1].hist(g2, alpha=0.5, label='Gentoo', bins=15, edgecolor='black')
axes[1].axvline(g1.median(), color='blue', linestyle='--', linewidth=2, label=f'Adelie median')
axes[1].axvline(g2.median(), color='orange', linestyle='--', linewidth=2, label=f'Gentoo median')
axes[1].set_title("Distribution of Bill Length")
axes[1].set_xlabel("Bill Length (mm)")
axes[1].set_ylabel("Frequency")
axes[1].legend()

plt.tight_layout()
plt.show()

예제: t-검정과 비교

# 독립표본 t-검정과 비교
from scipy.stats import ttest_ind

t_stat, p_t = ttest_ind(g1, g2)

print("\n=== 모수 vs 비모수 비교 ===")
print(f"t-검정 (모수):        t = {t_stat:.4f}, p = {p_t:.4f}")
print(f"Mann-Whitney (비모수): U = {u_stat:.4f}, p = {p_value:.4f}")
print("\n→ 두 검정 모두 유사한 결론")

=== 모수 vs 비모수 비교 ===
t-검정 (모수):        t = -25.0953, p = 0.0000
Mann-Whitney (비모수): U = 224.5000, p = 0.0000

→ 두 검정 모두 유사한 결론

17.3 Wilcoxon Signed-Rank Test

Wilcoxon 부호순위 검정은 동일 대상의 전후 변화를 비교하는 비모수 검정으로, 대응표본 t-검정의 대안이다.

특징

  • 대상: 동일 대상의 두 측정값 (대응 표본)
  • 대안 검정: 대응표본 t-검정
  • 검정 대상: 차이의 중앙값이 0인지
  • 가정: 차이의 분포가 대칭

가설 설정

  • H₀ (귀무가설): 차이의 중앙값이 0이다 (전후 차이 없음)
  • H₁ (대립가설): 차이의 중앙값이 0이 아니다 (전후 차이 있음)

검정 원리

  1. 각 쌍의 차이 계산
  2. 차이의 절댓값에 순위 매김
  3. 양수 차이와 음수 차이의 순위 합 비교

17.3.1 예제: 가상 전후 데이터

penguins 데이터셋에는 자연스러운 전후 데이터가 없으므로, 예제를 위해 가상 데이터를 생성한다.

예제: 가상 데이터 생성

from scipy.stats import wilcoxon

# 시드 설정
np.random.seed(42)

# 전후 데이터 생성 (예: 처치 전후 부리 길이)
before = df[df["species"] == "Adelie"]["bill_length_mm"].dropna().sample(30, random_state=42)
after = before + np.random.normal(0.5, 1.0, size=len(before))

print("=== 전후 데이터 ===")
print(f"처치 전 중앙값: {before.median():.2f}mm")
print(f"처치 후 중앙값: {after.median():.2f}mm")
print(f"차이 중앙값: {(after - before).median():.2f}mm")
=== 전후 데이터 ===
처치 전 중앙값: 39.25mm
처치 후 중앙값: 39.04mm
차이 중앙값: 0.27mm

예제: Wilcoxon Signed-Rank 검정

# Wilcoxon 부호순위 검정
w_stat, p_value = wilcoxon(before, after)

print("\n=== Wilcoxon Signed-Rank 검정 ===")
print(f"W-통계량: {w_stat:.4f}")
print(f"p-value: {p_value:.4f}")

alpha = 0.05
print(f"\n유의수준 {alpha} 기준:")
if p_value < alpha:
    print("✓ 귀무가설 기각: 처치 전후 유의한 차이 있음")
else:
    print("✗ 귀무가설 채택: 처치 전후 유의한 차이 없음")

=== Wilcoxon Signed-Rank 검정 ===
W-통계량: 149.0000
p-value: 0.0879

유의수준 0.05 기준:
✗ 귀무가설 채택: 처치 전후 유의한 차이 없음

예제: 시각화

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 전후 비교 박스플롯
data_paired = pd.DataFrame({
    'Value': pd.concat([before, after]),
    'Time': ['Before']*len(before) + ['After']*len(after)
})
sns.boxplot(x='Time', y='Value', data=data_paired, ax=axes[0])
axes[0].set_title("Before vs After Treatment")
axes[0].set_ylabel("Bill Length (mm)")

# 차이 분포
diff = after.values - before.values
axes[1].hist(diff, bins=15, edgecolor='black', alpha=0.7)
axes[1].axvline(0, color='red', linestyle='--', linewidth=2, label='No difference')
axes[1].axvline(np.median(diff), color='green', linestyle='--', linewidth=2, label=f'Median diff = {np.median(diff):.2f}')
axes[1].set_title("Distribution of Differences (After - Before)")
axes[1].set_xlabel("Difference (mm)")
axes[1].set_ylabel("Frequency")
axes[1].legend()

plt.tight_layout()
plt.show()

예제: 대응표본 t-검정과 비교

# 대응표본 t-검정과 비교
from scipy.stats import ttest_rel

t_stat, p_t = ttest_rel(before, after)

print("\n=== 모수 vs 비모수 비교 ===")
print(f"Paired t-test (모수):  t = {t_stat:.4f}, p = {p_t:.4f}")
print(f"Wilcoxon (비모수):     W = {w_stat:.4f}, p = {p_value:.4f}")

=== 모수 vs 비모수 비교 ===
Paired t-test (모수):  t = -1.8979, p = 0.0677
Wilcoxon (비모수):     W = 149.0000, p = 0.0879

17.4 Kruskal-Wallis Test

Kruskal-Wallis 검정은 세 개 이상 독립 집단의 분포를 비교하는 비모수 검정으로, 일원분산분석(ANOVA)의 대안이다.

특징

  • 대상: 세 개 이상 독립 집단
  • 대안 검정: 일원분산분석 (One-way ANOVA)
  • 검정 대상: 모든 집단의 분포가 같은지
  • 가정: 독립성 (분포 형태 가정 불필요)

가설 설정

  • H₀ (귀무가설): 모든 집단의 분포가 동일하다
  • H₁ (대립가설): 적어도 하나의 집단 분포가 다르다

검정 원리

  1. 모든 데이터를 합쳐서 순위 매김
  2. 각 집단의 순위 평균 계산
  3. H 통계량 계산 (카이제곱 분포에 근사)

17.4.1 예제: 세 종의 부리 길이 비교

예제: 데이터 준비

from scipy.stats import kruskal

# 세 종의 부리 길이
groups = [
    df[df["species"] == sp]["bill_length_mm"].dropna()
    for sp in df["species"].unique()
]

print("=== 집단 정보 ===")
for species, group in zip(df["species"].unique(), groups):
    print(f"{species:12s}: n = {len(group):3d}, 중앙값 = {group.median():.2f}mm")
=== 집단 정보 ===
Adelie      : n = 151, 중앙값 = 38.80mm
Chinstrap   : n =  68, 중앙값 = 49.55mm
Gentoo      : n = 123, 중앙값 = 47.30mm

예제: Kruskal-Wallis 검정

# Kruskal-Wallis 검정
h_stat, p_value = kruskal(*groups)

print("\n=== Kruskal-Wallis 검정 ===")
print(f"H-통계량: {h_stat:.4f}")
print(f"자유도: {len(groups) - 1}")
print(f"p-value: {p_value:.4f}")

alpha = 0.05
print(f"\n유의수준 {alpha} 기준:")
if p_value < alpha:
    print("✓ 귀무가설 기각: 종 간 부리 길이 분포가 유의하게 다름")
    print("  → 어느 종 간 차이인지 확인하려면 사후검정 필요")
else:
    print("✗ 귀무가설 채택: 종 간 부리 길이 분포가 유의하게 다르지 않음")

=== Kruskal-Wallis 검정 ===
H-통계량: 244.1367
자유도: 2
p-value: 0.0000

유의수준 0.05 기준:
✓ 귀무가설 기각: 종 간 부리 길이 분포가 유의하게 다름
  → 어느 종 간 차이인지 확인하려면 사후검정 필요

예제: ANOVA와 비교

# 일원분산분석과 비교
from scipy.stats import f_oneway

f_stat, p_anova = f_oneway(*groups)

print("\n=== 모수 vs 비모수 비교 ===")
print(f"ANOVA (모수):          F = {f_stat:.4f}, p = {p_anova:.4f}")
print(f"Kruskal-Wallis (비모수): H = {h_stat:.4f}, p = {p_value:.4f}")
print("\n→ 두 검정 모두 유사한 결론")

=== 모수 vs 비모수 비교 ===
ANOVA (모수):          F = 410.6003, p = 0.0000
Kruskal-Wallis (비모수): H = 244.1367, p = 0.0000

→ 두 검정 모두 유사한 결론

예제: 시각화

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 박스플롯
df_clean = df[["species", "bill_length_mm"]].dropna()
sns.boxplot(x="species", y="bill_length_mm", data=df_clean, ax=axes[0])
axes[0].set_title("Bill Length by Species")
axes[0].set_ylabel("Bill Length (mm)")

# 바이올린 플롯
sns.violinplot(x="species", y="bill_length_mm", data=df_clean, ax=axes[1])
axes[1].set_title("Distribution of Bill Length")
axes[1].set_ylabel("Bill Length (mm)")

plt.tight_layout()
plt.show()

17.4.2 사후검정 (Post-hoc Test)

Kruskal-Wallis 검정이 유의하면 쌍별 비교를 수행한다.

예제: Dunn’s Test (사후검정)

# Dunn's test는 scikit-posthocs 패키지 사용
# import scikit_posthocs as sp
# dunn_results = sp.posthoc_dunn(df_clean, val_col='bill_length_mm', group_col='species')
# print(dunn_results)

# 대안: 쌍별 Mann-Whitney U 검정 (Bonferroni 보정)
from itertools import combinations

species_list = df["species"].unique()
n_comparisons = len(list(combinations(species_list, 2)))
alpha_bonf = 0.05 / n_comparisons

print(f"\n=== 쌍별 Mann-Whitney U (Bonferroni 보정) ===")
print(f"비교 횟수: {n_comparisons}")
print(f"보정된 유의수준: {alpha_bonf:.4f}\n")

for sp1, sp2 in combinations(species_list, 2):
    g1 = df[df["species"] == sp1]["bill_length_mm"].dropna()
    g2 = df[df["species"] == sp2]["bill_length_mm"].dropna()
    
    u, p = mannwhitneyu(g1, g2)
    
    print(f"{sp1:12s} vs {sp2:12s}: U = {u:.4f}, p = {p:.4f} ", end="")
    print("→ 유의" if p < alpha_bonf else "→ 비유의")

=== 쌍별 Mann-Whitney U (Bonferroni 보정) ===
비교 횟수: 3
보정된 유의수준: 0.0167

Adelie       vs Chinstrap   : U = 101.0000, p = 0.0000 → 유의
Adelie       vs Gentoo      : U = 224.5000, p = 0.0000 → 유의
Chinstrap    vs Gentoo      : U = 5323.5000, p = 0.0018 → 유의

17.5 Friedman Test

Friedman 검정은 세 개 이상의 대응 집단(동일 대상의 반복 측정)을 비교하는 비모수 검정으로, 반복측정 ANOVA의 대안이다.

특징

  • 대상: 세 개 이상 대응 집단 (동일 대상의 반복 측정)
  • 대안 검정: 반복측정 ANOVA
  • 검정 대상: 모든 조건의 분포가 같은지
  • 가정: 각 블록(대상) 내에서 순위 매김 가능

가설 설정

  • H₀ (귀무가설): 모든 조건의 분포가 동일하다
  • H₁ (대립가설): 적어도 하나의 조건 분포가 다르다

17.5.1 예제: 가상 반복 측정 데이터

예제: 가상 데이터 생성

from scipy.stats import friedmanchisquare

# 가상 반복 측정 데이터 (예: 3가지 처치 조건)
np.random.seed(123)

n = 30
cond1 = before.values
cond2 = before.values + np.random.normal(0.3, 0.8, size=len(before))
cond3 = before.values + np.random.normal(0.8, 0.8, size=len(before))

print("=== 조건별 중앙값 ===")
print(f"조건 1: {np.median(cond1):.2f}mm")
print(f"조건 2: {np.median(cond2):.2f}mm")
print(f"조건 3: {np.median(cond3):.2f}mm")
=== 조건별 중앙값 ===
조건 1: 39.25mm
조건 2: 39.97mm
조건 3: 39.71mm

예제: Friedman 검정

# Friedman 검정
f_stat, p_value = friedmanchisquare(cond1, cond2, cond3)

print("\n=== Friedman 검정 ===")
print(f"χ²-통계량: {f_stat:.4f}")
print(f"자유도: {3 - 1}")
print(f"p-value: {p_value:.4f}")

alpha = 0.05
print(f"\n유의수준 {alpha} 기준:")
if p_value < alpha:
    print("✓ 귀무가설 기각: 조건 간 분포가 유의하게 다름")
else:
    print("✗ 귀무가설 채택: 조건 간 분포가 유의하게 다르지 않음")

=== Friedman 검정 ===
χ²-통계량: 11.6667
자유도: 2
p-value: 0.0029

유의수준 0.05 기준:
✓ 귀무가설 기각: 조건 간 분포가 유의하게 다름

예제: 시각화

# 시각화
data_repeated = pd.DataFrame({
    'Value': np.concatenate([cond1, cond2, cond3]),
    'Condition': ['Cond1']*len(cond1) + ['Cond2']*len(cond2) + ['Cond3']*len(cond3),
    'Subject': list(range(len(cond1))) * 3
})

plt.figure(figsize=(10, 6))
sns.boxplot(x='Condition', y='Value', data=data_repeated)
plt.title("Repeated Measures: Three Conditions")
plt.ylabel("Bill Length (mm)")
plt.show()

17.6 모수 검정 vs 비모수 검정 종합

검정 방법 대응표

상황 모수 검정 비모수 검정 검정 대상
두 독립 집단 독립표본 t-검정 Mann-Whitney U 평균 vs 분포 위치
두 대응 집단 대응표본 t-검정 Wilcoxon Signed-Rank 평균 차이 vs 중앙값 차이
세 집단 이상 (독립) 일원분산분석 (ANOVA) Kruskal-Wallis 평균 vs 분포
세 집단 이상 (대응) 반복측정 ANOVA Friedman 평균 vs 분포

선택 기준

조건 권장 검정
정규성 만족 + 등분산 + n ≥ 30 모수 검정 (높은 검정력)
정규성 의심 또는 이상치 많음 비모수 검정 (강건함)
소표본 (n < 30) + 정규성 불확실 비모수 검정 (안전)
순서형 데이터 비모수 검정 (유일한 선택)

17.7 요약

이 장에서는 분포 가정이 필요 없는 비모수 검정을 학습했다. 주요 내용은 다음과 같다.

비모수 검정 핵심 요약

검정 용도 장점 단점
Mann-Whitney U 두 독립 집단 정규성 불필요, 이상치 강건 검정력 약간 낮음
Wilcoxon Signed-Rank 두 대응 집단 정규성 불필요, 대칭성만 가정 검정력 약간 낮음
Kruskal-Wallis 다집단 독립 분포 가정 불필요 어느 집단이 다른지 모름
Friedman 다집단 대응 반복측정 가능 사후검정 제한적

비모수 검정 사용 원칙

  1. 가정 확인: 정규성, 등분산성 검정으로 모수 검정 가능 여부 확인
  2. 표본 크기: n < 30이고 정규성 불확실하면 비모수 선택
  3. 이상치: 이상치가 많으면 비모수 검정이 더 안전
  4. 해석: 비모수는 중앙값/분포 위치 비교, 모수는 평균 비교
  5. 검정력: 가정 만족 시 모수 검정이 검정력 높음

실무 의사결정 흐름

데이터 확보
  ↓
정규성 검정
  ├─ 정규 → 등분산성 검정
  │          ├─ 등분산 → 모수 검정 (t-test, ANOVA)
  │          └─ 이분산 → Welch 검정 또는 비모수
  └─ 비정규 → 비모수 검정 (Mann-Whitney, Kruskal-Wallis)

주의사항

  • 비모수 검정은 평균이 아닌 중앙값 또는 분포 위치 비교
  • 검정력이 모수 검정보다 약간 낮음 (가정 만족 시)
  • 사후검정이 모수 검정만큼 체계적이지 않음
  • 효과 크기 측정이 제한적
  • 해석 시 “분포의 위치 차이”로 표현

비모수 검정은 가정을 만족하지 못할 때 안전하고 강건한 대안이다. 데이터의 특성과 분석 목적을 고려하여 모수 검정과 비모수 검정을 적절히 선택하는 것이 중요하다. 다음으로는 변수 간 관계를 분석하는 상관분석과 회귀분석을 학습할 수 있다.